﻿/**
 * Author: Junaid P V
 * License: New BSD License
 **/
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Org.AI.Neural
{
    /// <summary>
    /// Represents a neural network
    /// </summary>
    public class NeuralNetwork
    {
        /// <summary>
        /// List that holds all neurons in the network
        /// </summary>
        private List<Neuron> neurons=new List<Neuron>();

        /// <summary>
        /// Learning rate for learning peroid.
        /// </summary>
        private double learningRate;

        /// <summary>
        /// Momentum factor for learning period
        /// </summary>
        private double momentumFactor;

        private NeuralNetworkPredicates neuronPredicates;

        /// <summary>
        /// Number of neurons in the network
        /// </summary>
        private int neuronCount;

        /// <summary>
        /// Get all the neurons in the network
        /// </summary>
        public ReadOnlyCollection<Neuron> Neurons
        {
            get { return this.neurons.AsReadOnly(); }
        }

        /// <summary>
        /// Construct a neural network with specified learing rate
        /// and momentum factor.
        /// </summary>
        /// <param name="learningRate"></param>
        /// <param name="momentumFactor"></param>
        public NeuralNetwork(double learningRate, double momentumFactor)
        {
            this.learningRate = learningRate;
            this.momentumFactor = momentumFactor;
        }

        /// <summary>
        /// This method must be called after all connections are set up and prior to
        /// using the network for working.
        /// </summary>
        public void PrepareToWork()
        {
            foreach (Neuron neuron in this.neurons)
            {
                neuron.Initialize();
            }
        }

        /// <summary>
        /// Initialize the neural network
        /// </summary>
        public void Initialize()
        {
            this.neurons = new List<Neuron>();
            this.neuronPredicates = new NeuralNetworkPredicates();
        }

        /// <summary>
        /// Create a connection from fromNeuron to toNeuron.
        /// Output of fromNeuron is fed to input of toNeuron
        /// </summary>
        /// <param name="fromNeuron">Connection starting neuron</param>
        /// <param name="toNeuron">Connection ending neuron</param>
        public void AddConnection(Neuron fromNeuron, Neuron toNeuron)
        {
            fromNeuron.AddOutputConnection(toNeuron);
            toNeuron.AddInputConnection(fromNeuron);
        }

        /// <summary>
        /// Create connection from fromNeuron to all given array of neurons.
        /// </summary>
        /// <param name="fromNeuron"></param>
        /// <param name="toNeurons"></param>
        public void AddConnections(Neuron fromNeuron, Neuron[] toNeurons)
        {
            foreach (Neuron n in toNeurons)
            {
                this.AddConnection(fromNeuron, n);
            }
        }

        /// <summary>
        /// Add connection from all neurons in the array fromNeurons to all neurons
        /// in the array toNeurons.
        /// </summary>
        /// <param name="fromNeurons"></param>
        /// <param name="toNeurons"></param>
        public void AddConnections(Neuron[] fromNeurons, Neuron[] toNeurons)
        {
            foreach (Neuron fromNeuron in fromNeurons)
            {
                this.AddConnections(fromNeuron, toNeurons);
            }
        }

        /// <summary>
        /// Construct a new neuron and it will be added to the specified layer.
        /// </summary>
        /// <param name="layer"></param>
        /// <returns></returns>
        public Neuron AddNeuron(int layer)
        {
            Neuron neuron = new Neuron(layer,this.neuronCount++);

            this.neurons.Add(neuron);
            return neuron;
        }

        /// <summary>
        /// Create connection between neurons where their id's are given
        /// </summary>
        /// <param name="fromId">Id of neuron where connection start</param>
        /// <param name="toId">Id of neuron where connection end</param>
        public void AddConnection(int fromId, int toId)
        {
            this.neuronPredicates.id=fromId;
            Neuron fromNeuron=this.neurons.Find(this.neuronPredicates.NeuronWithId);
            this.neuronPredicates.id=toId;
            Neuron toNeuron=this.neurons.Find(this.neuronPredicates.NeuronWithId);
            this.AddConnection(fromNeuron, toNeuron);
        }

        /// <summary>
        /// Return list of neurons in the specified layer.
        /// </summary>
        /// <param name="layer">layer number</param>
        /// <returns>list of neurons</returns>
        public ReadOnlyCollection<Neuron> GetNeurons(int layer)
        {
            this.neuronPredicates.layer=layer;
            return new ReadOnlyCollection<Neuron>(this.neurons.FindAll(this.neuronPredicates.NeuronOnLayer));
        }

        /// <summary>
        /// Number of neurons in the network
        /// </summary>
        public int Count
        {
            get { return this.neurons.Count; }
        }
        

        /// <summary>
        /// Get number of neurons in the specified layer.
        /// </summary>
        /// <param name="layer">layer number</param>
        /// <returns></returns>
        public int GetNumberOfNeurons(int layer)
        {
            this.neuronPredicates.layer = layer;
            return this.neurons.FindAll(this.neuronPredicates.NeuronOnLayer).Count;
        }

        /// <summary>
        /// Calculate output of all neurons in the given layer.
        /// </summary>
        /// <param name="layer">layer number</param>
        public void CalculateOutput(int layer)
        {
            ReadOnlyCollection<Neuron> neuronsInLayer = this.GetNeurons(layer);
            foreach (Neuron neuron in neuronsInLayer)
            {
                neuron.CalculateOutput();
            }
        }

        /// <summary>
        /// Outputs of neurons in the specified layer are fed to inputs of neurons
        /// which are connected to outputs of these neurons.
        /// </summary>
        /// <param name="layer">layer number</param>
        public void FeedOutputsForward(int layer)
        {
            ReadOnlyCollection<Neuron> neuronInLayer = this.GetNeurons(layer);
            foreach (Neuron neuron in neuronInLayer)
            {
                neuron.FeedOutputForward();
            }
        }

        /// <summary>
        /// Get a place holder array for inputs.
        /// So we can put inputs in this array and feed to network later with
        /// SetInputArray() method.
        /// </summary>
        /// <returns>place holder array for inputs.</returns>
        public double[] GetInputPlaceholderArray()
        {
            int numberOfInputs = 0;
            List<Neuron> inputNeurons = new List<Neuron>(this.GetNeurons(0));
            inputNeurons.Sort();
            foreach (Neuron neuron in inputNeurons)
            {
                numberOfInputs += neuron.GetNetworkInputConnections.Count;
            }
            return new double[numberOfInputs];
        }

        /// <summary>
        /// Set input values to the network.
        /// inputs are numbered by ordering neurons by its id number,
        /// and in each input neuron input connection are in order as they are added.
        /// </summary>
        /// <param name="inputArray">arraya of input values to the network</param>
        public void SetInputArray(double[] inputArray)
        {
            if (this.GetInputPlaceholderArray().Length != inputArray.Length)
            {
                throw new OverflowException("Size of Input Array not match");
            }
            else
            {
                List<Neuron> inputNeurons = new List<Neuron>(this.GetNeurons(0));
                inputNeurons.Sort();
                int i=0;
                foreach (Neuron neuron in inputNeurons)
                {
                    foreach (NeuronConnection connection in neuron.GetNetworkInputConnections)
                    {
                        connection.Input = inputArray[i];
                        i++;
                    }
                }
            }
        }

        /// <summary>
        /// Get outputs values out of network
        /// </summary>
        /// <returns>array of output values</returns>
        public double[] GetOutput()
        {
            List<Neuron> outputNeurons = new List<Neuron>(this.GetNeurons(-1));
            outputNeurons.Sort();
            double[] outputs = new double[outputNeurons.Count];
            int i = 0;
            foreach (Neuron neuron in outputNeurons)
            {
                outputs[i] = neuron.Output;
                i++;
            }
            return outputs;
        }

        /// <summary>
        /// Calculate errors on neurons on specified layer
        /// </summary>
        /// <param name="layer"></param>
        public void CalculateError(int layer)
        {
            ReadOnlyCollection<Neuron> neurons = this.GetNeurons(layer);
            foreach (Neuron neuron in neurons)
            {
                neuron.CalculateError();
            }
        }

        /// <summary>
        /// Adjust weights of the neurons in the specified layer
        /// </summary>
        /// <param name="layer">layer number</param>
        public void AdjustWeights(int layer)
        {
            ReadOnlyCollection<Neuron> neurons = this.GetNeurons(layer);
            foreach (Neuron neuron in neurons)
            {
                neuron.AdjustWeights(this.learningRate, this.momentumFactor);
            }
        }
    }
}
